<!DOCTYPE html>
<html>
<head>
    

    

    



    <meta charset="utf-8">
    
    
    
    <title>Android架构组件(一)-指导 | 随手记 | 不积跬步无以至千里</title>
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    
    <meta name="theme-color" content="#1c9d5c">
    
    
    <meta name="keywords" content="Android,Architecture Components">
    <meta name="description" content="这份文档用于已经掌握构建Android app基本技能，现在想要了解推荐的架构，想要实践如何构建健壮、生产级别app的开发者。  注意：本文档假设读者已经熟悉Android框架。如果你刚跟接触Android，请访问[这里]的训练系列，该训练包含了本文档的所有预备知识。  App开发者所面临的常见问题 与之对应的传统桌面应用在大多数情况下含有一个单一的入口点（快捷图标）并运行作为一个单一的程序，这和">
<meta name="keywords" content="Android,Architecture Components">
<meta property="og:type" content="article">
<meta property="og:title" content="Android架构组件(一)-指导">
<meta property="og:url" content="http://yoursite.com/2017/09/10/architecture-guide/index.html">
<meta property="og:site_name" content="随手记">
<meta property="og:description" content="这份文档用于已经掌握构建Android app基本技能，现在想要了解推荐的架构，想要实践如何构建健壮、生产级别app的开发者。  注意：本文档假设读者已经熟悉Android框架。如果你刚跟接触Android，请访问[这里]的训练系列，该训练包含了本文档的所有预备知识。  App开发者所面临的常见问题 与之对应的传统桌面应用在大多数情况下含有一个单一的入口点（快捷图标）并运行作为一个单一的程序，这和">
<meta property="og:locale" content="zh-CN">
<meta property="og:image" content="http://yoursite.com/2017/09/10/architecture-guide/final-architecture.png">
<meta property="og:image" content="http://yoursite.com/2017/09/10/architecture-guide/network-bound-resource.png">
<meta property="og:updated_time" content="2018-04-11T08:49:40.569Z">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="Android架构组件(一)-指导">
<meta name="twitter:description" content="这份文档用于已经掌握构建Android app基本技能，现在想要了解推荐的架构，想要实践如何构建健壮、生产级别app的开发者。  注意：本文档假设读者已经熟悉Android框架。如果你刚跟接触Android，请访问[这里]的训练系列，该训练包含了本文档的所有预备知识。  App开发者所面临的常见问题 与之对应的传统桌面应用在大多数情况下含有一个单一的入口点（快捷图标）并运行作为一个单一的程序，这和">
<meta name="twitter:image" content="http://yoursite.com/2017/09/10/architecture-guide/final-architecture.png">
    
        <link rel="alternate" type="application/atom+xml" title="随手记" href="/atom.xml">
    
    <link rel="shortcut icon" href="/favicon.ico">
    <link rel="stylesheet" href="/css/style.css?v=1.7.2">
    <script>window.lazyScripts=[]</script>

    <!-- custom head -->
    

</head>

<body>
    <div id="loading" class="active"></div>

    <aside id="menu" class="hide" >
  <div class="inner flex-row-vertical">
    <a href="javascript:;" class="header-icon waves-effect waves-circle waves-light" id="menu-off">
        <i class="icon icon-lg icon-close"></i>
    </a>
    <div class="brand-wrap" style="background-image:url(/img/brand.jpg)">
      <div class="brand">
        <a href="/" class="avatar waves-effect waves-circle waves-light">
          <img src="/img/avatar.jpg">
        </a>
        <hgroup class="introduce">
          <h5 class="nickname">马建</h5>
          <a href="mailto:markjoker@126.com" title="markjoker@126.com" class="mail">markjoker@126.com</a>
        </hgroup>
      </div>
    </div>
    <div class="scroll-wrap flex-col">
      <ul class="nav">
        
            <li class="waves-block waves-effect">
              <a href="/"  >
                <i class="icon icon-lg icon-home"></i>
                主页
              </a>
            </li>
        
            <li class="waves-block waves-effect">
              <a href="/archives"  >
                <i class="icon icon-lg icon-archives"></i>
                归档
              </a>
            </li>
        
            <li class="waves-block waves-effect">
              <a href="/tags"  >
                <i class="icon icon-lg icon-tags"></i>
                标签
              </a>
            </li>
        
            <li class="waves-block waves-effect">
              <a href="/categories"  >
                <i class="icon icon-lg icon-th-list"></i>
                分类
              </a>
            </li>
        
            <li class="waves-block waves-effect">
              <a href="https://github.com/markjoker" target="_blank" >
                <i class="icon icon-lg icon-github"></i>
                Github
              </a>
            </li>
        
            <li class="waves-block waves-effect">
              <a href="http://www.weibo.com/markjoker" target="_blank" >
                <i class="icon icon-lg icon-weibo"></i>
                微博
              </a>
            </li>
        
            <li class="waves-block waves-effect">
              <a href="/cv"  >
                <i class="icon icon-lg icon-file"></i>
                简历
              </a>
            </li>
        
      </ul>
    </div>
  </div>
</aside>

    <main id="main">
        <header class="top-header" id="header">
    <div class="flex-row">
        <a href="javascript:;" class="header-icon waves-effect waves-circle waves-light on" id="menu-toggle">
          <i class="icon icon-lg icon-navicon"></i>
        </a>
        <div class="flex-col header-title ellipsis">Android架构组件(一)-指导</div>
        
        <div class="search-wrap" id="search-wrap">
            <a href="javascript:;" class="header-icon waves-effect waves-circle waves-light" id="back">
                <i class="icon icon-lg icon-chevron-left"></i>
            </a>
            <input type="text" id="key" class="search-input" autocomplete="off" placeholder="输入感兴趣的关键字">
            <a href="javascript:;" class="header-icon waves-effect waves-circle waves-light" id="search">
                <i class="icon icon-lg icon-search"></i>
            </a>
        </div>
        
        
        <a href="javascript:;" class="header-icon waves-effect waves-circle waves-light" id="menuShare">
            <i class="icon icon-lg icon-share-alt"></i>
        </a>
        
    </div>
</header>
<header class="content-header post-header">

    <div class="container fade-scale">
        <h1 class="title">Android架构组件(一)-指导</h1>
        <h5 class="subtitle">
            
                <time datetime="2017-09-10T08:24:13.000Z" itemprop="datePublished" class="page-time">
  2017-09-10
</time>


            
        </h5>
    </div>

    


</header>


<div class="container body-wrap">
    
    <aside class="post-widget">
        <nav class="post-toc-wrap post-toc-shrink" id="post-toc">
            <h4>目录</h4>
            <ol class="post-toc"><li class="post-toc-item post-toc-level-1"><a class="post-toc-link" href="#App开发者所面临的常见问题"><span class="post-toc-number">1.</span> <span class="post-toc-text">App开发者所面临的常见问题</span></a></li><li class="post-toc-item post-toc-level-1"><a class="post-toc-link" href="#常见架构原则"><span class="post-toc-number">2.</span> <span class="post-toc-text">常见架构原则</span></a></li><li class="post-toc-item post-toc-level-1"><a class="post-toc-link" href="#推荐app架构"><span class="post-toc-number">3.</span> <span class="post-toc-text">推荐app架构</span></a><ol class="post-toc-child"><li class="post-toc-item post-toc-level-2"><a class="post-toc-link" href="#构建用户界面"><span class="post-toc-number">3.1.</span> <span class="post-toc-text">构建用户界面</span></a></li><li class="post-toc-item post-toc-level-2"><a class="post-toc-link" href="#获取数据"><span class="post-toc-number">3.2.</span> <span class="post-toc-text">获取数据</span></a><ol class="post-toc-child"><li class="post-toc-item post-toc-level-3"><a class="post-toc-link" href="#管理组件间的依赖"><span class="post-toc-number">3.2.1.</span> <span class="post-toc-text">管理组件间的依赖</span></a></li></ol></li><li class="post-toc-item post-toc-level-2"><a class="post-toc-link" href="#连接ViewModel和仓库"><span class="post-toc-number">3.3.</span> <span class="post-toc-text">连接ViewModel和仓库</span></a></li><li class="post-toc-item post-toc-level-2"><a class="post-toc-link" href="#缓存数据"><span class="post-toc-number">3.4.</span> <span class="post-toc-text">缓存数据</span></a></li><li class="post-toc-item post-toc-level-2"><a class="post-toc-link" href="#数据持久化"><span class="post-toc-number">3.5.</span> <span class="post-toc-text">数据持久化</span></a><ol class="post-toc-child"><li class="post-toc-item post-toc-level-4"><a class="post-toc-link" href="#真正单一数据源"><span class="post-toc-number">3.5.0.1.</span> <span class="post-toc-text">真正单一数据源</span></a></li></ol></li></ol></li><li class="post-toc-item post-toc-level-2"><a class="post-toc-link" href="#测试"><span class="post-toc-number">3.6.</span> <span class="post-toc-text">测试</span></a></li><li class="post-toc-item post-toc-level-2"><a class="post-toc-link" href="#最终架构"><span class="post-toc-number">3.7.</span> <span class="post-toc-text">最终架构</span></a></li></ol></li><li class="post-toc-item post-toc-level-1"><a class="post-toc-link" href="#指导原则"><span class="post-toc-number">4.</span> <span class="post-toc-text">指导原则</span></a></li><li class="post-toc-item post-toc-level-1"><a class="post-toc-link" href="#附加：显示网络状态"><span class="post-toc-number">5.</span> <span class="post-toc-text">附加：显示网络状态</span></a></li></ol>
        </nav>
    </aside>


<article id="post-architecture-guide"
  class="post-article article-type-post fade" itemprop="blogPost">

    <div class="post-card">
        <h1 class="post-card-title">Android架构组件(一)-指导</h1>
        <div class="post-meta">
            <time class="post-time" title="2017-09-10 16:24:13" datetime="2017-09-10T08:24:13.000Z"  itemprop="datePublished">2017-09-10</time>

            


            
<span id="busuanzi_container_page_pv" title="文章总阅读量" style='display:none'>
    <i class="icon icon-eye icon-pr"></i><span id="busuanzi_value_page_pv"></span>
</span>


        </div>
        <div class="post-content" id="post-content" itemprop="postContent">
            <p>这份文档用于已经掌握构建Android app基本技能，现在想要了解推荐的架构，想要实践如何构建健壮、生产级别app的开发者。</p>
<blockquote>
<p><strong>注意：</strong>本文档假设读者已经熟悉Android框架。如果你刚跟接触Android，请访问[这里]的训练系列，该训练包含了本文档的所有预备知识。</p>
</blockquote>
<h1 id="App开发者所面临的常见问题"><a href="#App开发者所面临的常见问题" class="headerlink" title="App开发者所面临的常见问题"></a>App开发者所面临的常见问题</h1><hr>
<p>与之对应的传统桌面应用在大多数情况下含有一个单一的入口点（快捷图标）并运行作为一个单一的程序，这和Android应用很不同。Android app拥有更复杂的结构。一个典型的Android app往往由多种组件构建而成，包括Activity, Fragment, Service, Content Provider以及Broadcast Receiver。</p>
<p>这些app组件大部分被声明在app清单文件（AndroidManifest）中，该清单文件被Android系统用于决定如何整合你的app到全局的用户体验中。如上文所说，传统的桌面应用通常作为一个整体运行，而一个编写良好的Android应用需要更加灵活，因为用户常常在不同的app间频繁切换。</p>
<p>例如，考虑当你想在你最喜欢的社交网络上分享一张照片时会发生什么？app触发一个相机的Intent，Android系统启动了一个相机应用来处理请求。在这个时候，用户离开了该社交网络app，但是在体验上却是无缝衔接的。接着，相机app可能触发其他Intent来开启其他应用，例如启动文件选择器。最终，用户回到了社交网络app并分享了图片。同样地，用户可能在这一处理过程中的任何时刻被电话接听所打断，在接听完成后继续回来分享图片。</p>
<p>在Android中，这种应用频繁切换的行为很常见，因此你的app必须能够正确处理这些行为。请记住，手机设备是被资源所约束的，因此在任何时候操作系统都有可能为了给新开启的app腾出空间而杀死一些app。</p>
<p>关于这一切的关键点在于你的app组件可以单独启动并且是无序的，以及该组件可以在任何时候被用户或系统销毁。因为app组件是短暂的，并且它们的生命周期（例如何时创建以及何时销毁）并不受你控制。你不能在你的app组件中存储任何数据或状态，并且你的组件之间不应该互相依赖。</p>
<h1 id="常见架构原则"><a href="#常见架构原则" class="headerlink" title="常见架构原则"></a>常见架构原则</h1><hr>
<p>如果你不能使用app组件来存储应用的数据和状态，那么app该如何构建呢？</p>
<p>你所该关注最重要的事情是在你的app中遵守关注点分离原则。一个常见的错误是把你所有的代码都写在Activity或者Fragment中。任何不操作UI或操作系统交互的代码都不应该放在上述这些类中。请尽量保持这些类的体积瘦小以避免许多生命周期相关的问题。不要忘记你并不拥有这些类，它们只是在你的应用和系统之间交互的粘合剂。安卓系统会在任何时候销毁它们，例如用户的交互行为或者其他因素，如可用内存过低等。为了提供一个可靠的用户体验，最好减少对它们的依赖。</p>
<p>第二个最重要的原则是你应该用模型驱动界面，最好是持久化模型(Persistent Model)。持久化是一个理想的状态，理由如下：1.如果操作系统销毁了你的应用来释放资源，你的用户不应该因此而丢掉数据。2.甚至当网络堵塞甚至未连接时，你的应用应当继续工作。Model是负责处理应用数据的组件，它们独立于视图(View)以及其他app组件，因此Model和这些生命周期相关的问题也是隔绝的。保持UI代码的简洁以及应用逻辑的自由更易于进行管理。将你的app基于Model类构建将对数据管理有利，并使得它们易于测试。</p>
<h1 id="推荐app架构"><a href="#推荐app架构" class="headerlink" title="推荐app架构"></a>推荐app架构</h1><hr>
<p>在这一章节，我们致力于如何使用架构组件(Architecture Components)来构建一个app，我们将通过一个用例进行说明。</p>
<blockquote>
<p><strong>注意：</strong>软件工程领域没有银弹。我们不可能找到一种最佳的方法能够一劳永逸地适合所有的场景。但是我们所推荐架构的意义在于对大多数用例来说都是好的。如果你已经有一个比较好的方式来写Android应用，那么你不需要做出改变。</p>
</blockquote>
<p>想象一下我们正在构建一个显示用户资料的UI界面。该用户界面将通过REST API从我们的私有后台获取。</p>
<h2 id="构建用户界面"><a href="#构建用户界面" class="headerlink" title="构建用户界面"></a>构建用户界面</h2><p>UI界面将会由一个叫做<code>UserProfileFragment.java</code>的Fragment和对应的布局文件<code>user_profile_layout.xml</code>组成。</p>
<p>为了驱动UI界面，我们的数据模型需要持有两个数据元素：</p>
<ul>
<li><strong>User ID:</strong>用于区分用户。通过fragment参数将信息传递至Fragment是最佳的方式。如果Android系统销毁了你的进程，这个信息将会被保存，因此当app下次重启时，该id也将是可用的</li>
<li><strong>User Object:</strong>一个含有用户数据的POJO类</li>
</ul>
<p>我们将会创建一个基于<strong>ViewModel</strong>类的<code>UserProfileViewModel</code>来保存信息。</p>
<p>一个ViewModel提供了指定UI组件的数据，例如一个fragment或activity，并处理数据的交互，例如调用其他组件加载数据或数据的更新修改等。ViewModel并不知道View，也不受配置信息变化的影响，例如由于屏幕旋转造成的Activity重建。</p>
<p>现在我们拥有以下三个文件：</p>
<ul>
<li><strong>user_profile.xml: </strong>定义了屏幕的UI布局</li>
<li><strong>UserProfileViewModel.java:</strong>准备用于UI的数据类</li>
<li><strong>UserProfileFragment.java: </strong>UI控制器，在ViewModel中显示数据以及响应用户交互</li>
</ul>
<p>下面是我们的初步实现（布局文件比较简单直接省略）：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">UserProfileViewModel</span> <span class="keyword">extends</span> <span class="title">ViewModel</span> </span>&#123;</span><br><span class="line">     <span class="keyword">private</span> String userId;</span><br><span class="line">     <span class="keyword">private</span> User user;</span><br><span class="line"> </span><br><span class="line">     <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">init</span><span class="params">(String userId)</span> </span>&#123;</span><br><span class="line">         <span class="keyword">this</span>.userId = userId;</span><br><span class="line">     &#125;</span><br><span class="line">     <span class="function"><span class="keyword">public</span> User <span class="title">getUser</span><span class="params">()</span> </span>&#123;</span><br><span class="line">         <span class="keyword">return</span> user;</span><br><span class="line">     &#125;</span><br><span class="line"> &#125;</span><br></pre></td></tr></table></figure></p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">UserProfileFragment</span> <span class="keyword">extends</span> <span class="title">LifecycleFragment</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> String UID_KEY = <span class="string">"uid"</span>;</span><br><span class="line">    <span class="keyword">private</span> UserProfileViewModel viewModel;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onActivityCreated</span><span class="params">(@Nullable Bundle savedInstanceState)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">super</span>.onActivityCreated(savedInstanceState);</span><br><span class="line">        String userId = getArguments().getString(UID_KEY);</span><br><span class="line">        viewModel = ViewModelProviders.of(<span class="keyword">this</span>).get(UserProfileViewModel.class);</span><br><span class="line">        viewModel.init(userId);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> View <span class="title">onCreateView</span><span class="params">(LayoutInflater inflater,</span></span></span><br><span class="line"><span class="function"><span class="params">                @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> inflater.inflate(R.layout.user_profile, container, <span class="keyword">false</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<blockquote>
<p><strong>注意：</strong>如果你已经使用了类似于RxJava或者Agera这样的库，你可以继续使用它们，而不是LiveData。但是如果当你使用它们，请确保正确地处理了生命周期，例如当相关的生命周期拥有者（LifecycleOwner）停止时应当暂停，当生命周期持有者销毁时也应当销毁。你也可以添加android.arch.lifecycle:reactivestreams，使LiveData和其他响应流式库共同使用，例如RxJava。</p>
</blockquote>
<p>现在我们将<code>UserProfileViewModel</code>中的User成员变量替换为<code>LiveData&lt;User&gt;</code>，使得当数据更新时，Fragment可以收到通知。关于<code>LiveData</code>一件很棒的事是，它能够对生命周期做出反应，并将在不再需要的时候自动清除引用。<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">UserProfileViewModel</span> <span class="keyword">extends</span> <span class="title">ViewModel</span> </span>&#123;</span><br><span class="line">     <span class="comment">//...</span></span><br><span class="line">     <span class="keyword">private</span> LiveData&lt;User&gt; user;<span class="comment">//替换行</span></span><br><span class="line">     <span class="function"><span class="keyword">public</span> LiveData&lt;User&gt; <span class="title">getUser</span><span class="params">()</span> </span>&#123;</span><br><span class="line">         <span class="keyword">return</span> user;</span><br><span class="line">     &#125;</span><br><span class="line"> &#125;</span><br></pre></td></tr></table></figure></p>
<p>现在我们修改UserProfileFragment，观察数据变化并更新UI。<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onActivityCreated</span><span class="params">(@Nullable Bundle savedInstanceState)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">super</span>.onActivityCreated(savedInstanceState);</span><br><span class="line">    viewModel.getUser().observe(<span class="keyword">this</span>, user -&gt; &#123;</span><br><span class="line">      <span class="comment">// 此处更新 UI</span></span><br><span class="line">    &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p>
<p>每次用户数据被更新时，<code>onChanged</code>回调函数会被调用，UI界面会被更新。</p>
<p>如果你熟悉其他使用观察回调的类库，你可能会意识到我们并没有复写Fragment的<code>onStop()</code>方法来停止对数据的观察。这在<code>LiveData中</code>是不必要的，因为它对生命周期敏感，这意味着将不会调用回调函数，除非Fragment出在激活状态（接收<code>onStart()</code>但没有接受<code>onStop()</code>）。当Fragment接收<code>onDestroy()</code>方法时，LiveData将会自动清除观察者。</p>
<p>我们也不会做任何特殊的事情来处理配置的变化（例如旋转屏幕）。当配置发生变化的时候，ViewModel将会自动保存，因此一旦新的Fragment到来时，它将会收到ViewModel的相同实例，带有当前数据的回调函数将会立即被调用。这就是ViewModel不应该直接引用View的原因，ViewModel会在View的生命周期外存活。详见：[ViewModel的生命周期]。</p>
<h2 id="获取数据"><a href="#获取数据" class="headerlink" title="获取数据"></a>获取数据</h2><p>现在我们将ViewModel和Fragment关联在了一起，但是ViewModel该如何获取数据呢？在本例下，我们假设我们的后台提供了REST API。我们会用<code>Retrofit</code>库来访问我们的后台，当然你可以随意选择其他不同的类库。</p>
<p>这里就是和我们后台交互的retrofit接口Webservice：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">Webservice</span> </span>&#123;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@GET</span> 声明是一个HTTP GET请求</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@Path</span>("user") 标记了userId参数来替换GET请求中的&#123;user&#125;路径</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@GET</span>(<span class="string">"/users/&#123;user&#125;"</span>)</span><br><span class="line">    <span class="function">Call&lt;User&gt; <span class="title">getUser</span><span class="params">(@Path(<span class="string">"user"</span>)</span> String userId)</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p>
<p>关于Retrofit的使用请详见官方文档，这里只是简单进行了说明</p>
<p>ViewModel的原生实现可以直接调用Webservice来获取数据并交给用户对象。即使这样可以生效，你的app将会随着增长而难以维护。相对于我们上文所提到的关注点分离原则，这种方式给予了ViewModel类太多的职责。另外ViewModel的作用于被绑在Activity或Fragment的生命周期上，因此当生命周期结束的时候丢掉这些数据是一种很糟糕的用户体验。作为替代，我们的ViewModel将会把这一工作委派给新的仓库（Repository）模块。</p>
<p>仓库模块(Repository Module)负责处理数据操作。他们提供了清晰的API，并且知道在哪获取数据以及哪种API的调用会导致数据更新。你可以考虑把它作为多种数据源的中介（持久化模型，网络服务数据，缓存等）。</p>
<p>下方的<code>UserRepository</code>类将会使用<code>WebService</code>来获取数据项：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">UserRepository</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> Webservice webservice;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> LiveData&lt;User&gt; <span class="title">getUser</span><span class="params">(<span class="keyword">int</span> userId)</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 这并不是最佳的实现方式，我们将在下文修正它</span></span><br><span class="line">        <span class="keyword">final</span> MutableLiveData&lt;User&gt; data = <span class="keyword">new</span> MutableLiveData&lt;&gt;();</span><br><span class="line">        webservice.getUser(userId).enqueue(<span class="keyword">new</span> Callback&lt;User&gt;() &#123;</span><br><span class="line">            <span class="meta">@Override</span></span><br><span class="line">            <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onResponse</span><span class="params">(Call&lt;User&gt; call, Response&lt;User&gt; response)</span> </span>&#123;</span><br><span class="line">                <span class="comment">// 错误情况的处理被省略了</span></span><br><span class="line">                data.setValue(response.body());</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line">        <span class="keyword">return</span> data;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p>
<p>即使仓库模型看起来并不需要，但是它完成了一个重要的目标：它将app中的数据源抽象了出来。现在我们的ViewModel不知道数据是由Webservice获取而来的，这意味着在需要其他实现的时候我们可以进行替换。</p>
<h3 id="管理组件间的依赖"><a href="#管理组件间的依赖" class="headerlink" title="管理组件间的依赖"></a>管理组件间的依赖</h3><p>上面的<code>UserRepository</code>类需要<code>WebService</code>接口的一个实例去进行工作。我们当然可以在每个仓库模型类中简单地创建一个，不过需要知道<code>WebService</code>所依赖的具体子类。这将会显著提高代码的复杂性和冗余。另外<code>UserRepository</code> 也可能不是唯一需要<code>WebService</code>的类，如果每个类都创建一个<code>WebService</code>，这将会浪费很多的资源。</p>
<p>有两种模式可以解决这个问题：</p>
<ul>
<li><strong>依赖注入：</strong>依赖注入允许类定义依赖而不用去构造他们。在运行的时候，另一个类负责提供这些依赖关系。我们推荐在安卓中使用谷歌的[Dagger 2]类库进行依赖注入。通过遍历依赖树，Dagger 2 自动构造对象并提供编译时的依赖保障。</li>
<li><strong>服务定位：</strong>服务定位器提供了注册器，使得类可通过依赖进行构建，而不是需要配置它们。服务定位模式相对依赖注入而言更易于实现，因此如果你并不熟悉依赖注入，可以使用服务定位来代替。</li>
</ul>
<h2 id="连接ViewModel和仓库"><a href="#连接ViewModel和仓库" class="headerlink" title="连接ViewModel和仓库"></a>连接ViewModel和仓库</h2><p>现在我们修改我们的UserProfileViewModel以使用仓库：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">UserProfileViewModel</span> <span class="keyword">extends</span> <span class="title">ViewModel</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> LiveData&lt;User&gt; user;</span><br><span class="line">    <span class="keyword">private</span> UserRepository userRepo;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Inject</span> <span class="comment">// UserRepository 参数由Dagger 2提供</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">UserProfileViewModel</span><span class="params">(UserRepository userRepo)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.userRepo = userRepo;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">init</span><span class="params">(String userId)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (<span class="keyword">this</span>.user != <span class="keyword">null</span>) &#123;</span><br><span class="line">            <span class="comment">// ViewModel 由每个fragment创建，因此我们知道并不会发生改变</span></span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        user = userRepo.getUser(userId);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> LiveData&lt;User&gt; <span class="title">getUser</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">this</span>.user;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p>
<h2 id="缓存数据"><a href="#缓存数据" class="headerlink" title="缓存数据"></a>缓存数据</h2><p>上述仓库的实现易于抽象了调用网络服务的过程，但是因为它仅仅依赖于一个单一的数据源，因此并不是很实用。</p>
<p><code>UserRepository</code>实现的问题在于在获取数据以后，并没有在任何地方保存它。如果用户离开了<code>UserProfileFragment</code>并再次回来，app会重新获取数据。这很糟糕，有以下两个原因：1.浪费了宝贵的网络带宽；2.强迫用户等待新的请求完成。为了解决这个问题，我们将在<code>UserRepository</code>添加一个新的数据源在内存中缓存我们的User对象。<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Singleton</span>  <span class="comment">// 通知 Dagger 该类应该只构建一次</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">UserRepository</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> Webservice webservice;</span><br><span class="line">    <span class="comment">// 简单缓存在内存中，忽略实现细节</span></span><br><span class="line">    <span class="keyword">private</span> UserCache userCache;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> LiveData&lt;User&gt; <span class="title">getUser</span><span class="params">(String userId)</span> </span>&#123;</span><br><span class="line">        LiveData&lt;User&gt; cached = userCache.get(userId);</span><br><span class="line">        <span class="keyword">if</span> (cached != <span class="keyword">null</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> cached;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">final</span> MutableLiveData&lt;User&gt; data = <span class="keyword">new</span> MutableLiveData&lt;&gt;();</span><br><span class="line">        userCache.put(userId, data);</span><br><span class="line">        <span class="comment">// 这仍然不是最优的代码，但是要比之前的代码好</span></span><br><span class="line">        <span class="comment">// 一个完整的实现必须处理错误情况</span></span><br><span class="line">        webservice.getUser(userId).enqueue(<span class="keyword">new</span> Callback&lt;User&gt;() &#123;</span><br><span class="line">            <span class="meta">@Override</span></span><br><span class="line">            <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onResponse</span><span class="params">(Call&lt;User&gt; call, Response&lt;User&gt; response)</span> </span>&#123;</span><br><span class="line">                data.setValue(response.body());</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line">        <span class="keyword">return</span> data;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p>
<h2 id="数据持久化"><a href="#数据持久化" class="headerlink" title="数据持久化"></a>数据持久化</h2><p>在我们当前的实现中，如果用户旋转了屏幕或者离开并返回app，当前UI界面将立刻可见，这是因为仓库从内存中获取了数据。但是如果用户离开app很久，在Android系统杀掉进程后再回来呢？</p>
<p>在当前的实现中，我们需要从网络重新获取数据。这并不仅是一个很糟糕的用户习惯，并且很浪费，因为我们要重新获取相同的数据。你可以仅仅通过缓存网络请求来修复它，但是这也创造了新的问题。如果相同的数据类型在另一个请求中发生（如获取一组好友列表）呢？如果是这样，你的app可能会显示不正确的数据。</p>
<p>正确解决这个问题的关键在于使用一个持久化模型。这正是<code>Room</code>持久化类库所解决的问题。</p>
<p><code>Room</code>是一个以最小化模板代码提供本地数据持久化的对象关系映射类库。在编译时间，它会验证每个查询语句，因此错误的SQL会导致编译时报错，而不是在运行时报错。Room抽象了一些原生SQL表和查询的底层实现细节。它也允许观察数据库数据的变化，通过LiveData对象进行展现。此外，它显式地定义线程约束以解决一些常见的问题，如在主线程访问存储。</p>
<blockquote>
<p><strong>注意：</strong>如果你对另一些持久化解决方案很熟悉，你并不需要进行替换，除非Room的功能集和你的用例更符合。</p>
</blockquote>
<p>为了使用<code>Room</code>，我们需要定义我们的本地表。首先使用<code>@Entity</code>去注解<code>User</code>类，标记该类作为数据库中的表。<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Entity</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">User</span> </span>&#123;</span><br><span class="line">  <span class="meta">@PrimaryKey</span></span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">int</span> id;</span><br><span class="line">  <span class="keyword">private</span> String name;</span><br><span class="line">  <span class="keyword">private</span> String lastName;</span><br><span class="line">  <span class="comment">// getters/setters</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p>
<p>之后，通过扩展<code>RoomDatabase</code>类创建一个数据库类：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Database</span>(entities = &#123;User.class&#125;, version = <span class="number">1</span>)</span><br><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">MyDatabase</span> <span class="keyword">extends</span> <span class="title">RoomDatabase</span> </span>&#123;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p>
<p>注意，<code>MyDatabase</code>类是抽象的，<code>Room</code>会自动提供实现。详情请参见Room文档。</p>
<p>现在我们需要一个方式将用户数据插入到数据库中，为此我们需要创建一个数据访问对象(DAO)：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Dao</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">UserDao</span> </span>&#123;</span><br><span class="line">    <span class="meta">@Insert</span>(onConflict = REPLACE)</span><br><span class="line">    <span class="function"><span class="keyword">void</span> <span class="title">save</span><span class="params">(User user)</span></span>;</span><br><span class="line">    <span class="meta">@Query</span>(<span class="string">"SELECT * FROM user WHERE id = :userId"</span>)</span><br><span class="line">    <span class="function">LiveData&lt;User&gt; <span class="title">load</span><span class="params">(String userId)</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p>
<p>之后，从我们的数据库类中引用DAO：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Database</span>(entities = &#123;User.class&#125;, version = <span class="number">1</span>)</span><br><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">MyDatabase</span> <span class="keyword">extends</span> <span class="title">RoomDatabase</span> </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">abstract</span> UserDao <span class="title">userDao</span><span class="params">()</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p>
<p>请注意<code>load</code>方法返回了一个<code>LiveData&lt;User&gt;</code>。<code>Room</code>知道数据库什么时候被修改并将在数据变化时自动通知所有已激活的观察者。使用了LiveData是很高效的，因为只有在至少含有一个处在激活状态的观察者时才会更新。</p>
<blockquote>
<p><strong>注意：</strong>Room会检查基于表修改的错误信息，也就是说会分发假阳性的通知。假阳性是指分发的通知是正确的，但是并非是由数据变化所造成的。</p>
</blockquote>
<p>现在我们修改UserRepository类，将Room数据源包含在内。<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Singleton</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">UserRepository</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> Webservice webservice;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> UserDao userDao;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> Executor executor;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Inject</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">UserRepository</span><span class="params">(Webservice webservice, UserDao userDao, Executor executor)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.webservice = webservice;</span><br><span class="line">        <span class="keyword">this</span>.userDao = userDao;</span><br><span class="line">        <span class="keyword">this</span>.executor = executor;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> LiveData&lt;User&gt; <span class="title">getUser</span><span class="params">(String userId)</span> </span>&#123;</span><br><span class="line">        refreshUser(userId);</span><br><span class="line">        <span class="comment">//直接从数据库返回数据</span></span><br><span class="line">        <span class="keyword">return</span> userDao.load(userId);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">refreshUser</span><span class="params">(<span class="keyword">final</span> String userId)</span> </span>&#123;</span><br><span class="line">        executor.execute(() -&gt; &#123;</span><br><span class="line">            <span class="comment">// 运行在后台线程</span></span><br><span class="line">            <span class="comment">// 检查用户最新是否获取更新</span></span><br><span class="line">            <span class="keyword">boolean</span> userExists = userDao.hasUser(FRESH_TIMEOUT);</span><br><span class="line">            <span class="keyword">if</span> (!userExists) &#123;</span><br><span class="line">                <span class="comment">// 刷新数据</span></span><br><span class="line">                Response response = webservice.getUser(userId).execute();</span><br><span class="line">                <span class="comment">// TODO 错误情况监测处理（省略）</span></span><br><span class="line">                <span class="comment">// 更新数据库，LiveData会自动更新，因此只需要更新数据库就可以了</span></span><br><span class="line">                userDao.save(response.body());</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p>
<p>请注意，即使我们在<code>UserRepository</code>中改变了数据源，我们仍然不需要改变<code>UserProfileViewModel</code>或者<code>UserProfileFragment</code>。这种灵活性是由抽象所提供的。这对于测试来说也是很棒的，因为你可以在测试<code>UserProfileViewModel</code>的时候提供一个假的<code>UserRepository</code> 。</p>
<p>现在我们的代码完成了。如果用户稍后再次回到相同的UI，将会立即看到用户信息，因为我们进行了持久化。同时，如果数据过时了，我们的仓库会在后台更新数据它们。当然这取决于你的具体用例，你可以选择在数据过时的时候不显示它们。</p>
<p>在一些用例中，例如pull-to-refresh，对于UI来说如果当前在进行网络请求，对用户显示该进度是很重要的。将UI的行为和实际数据分离是一种很好的实践，因为数据可能因为多种原因被更新（例如如果我们拉取一组朋友列表，已存在的数据可能会被再次获取，从而触发了<code>LiveData&lt;User&gt;</code>更新）。从UI的角度来看，事实上是另一个数据端。</p>
<p>该用例有两个常见的方案：</p>
<ul>
<li>修改<code>getUser()</code>方法，返回带有网络操作状态的LiveData，例如下文中的“显示网络状态”章节。</li>
<li>在仓库类中提供另一个公共方法，返回User类的刷新状态。这种方式更好，如果你想要仅仅在响应显式地用户操作（如pull-to-refresh）时显示网络状态。</li>
</ul>
<h4 id="真正单一数据源"><a href="#真正单一数据源" class="headerlink" title="真正单一数据源"></a>真正单一数据源</h4><p>对于不同的REST API返回相同的数据是很常见的，例如，如果我们的后台有另一个接口用于返回朋友列表，相同的User对象会从两个API返回。如果<code>UserRepository</code>也要去返回<code>Webservice</code>请求的结果，我们的UI界面可能会显示不正常数据，因为数据可能会因这两个请求接口而改变。这也就是为什么在<code>UserRepository</code>实现中，网络服务仅仅存储数据到数据库的原因。之后，数据库信息的改变会触发<code>LiveData</code>的更新。</p>
<p>在这种模型下，数据库作为单一数据源，而app的其他部分通过仓库进行访问。不论你是否使用持久化存储，我们推荐你的仓库指定一个数据源作为app的单一数据源。</p>
<h2 id="测试"><a href="#测试" class="headerlink" title="测试"></a>测试</h2><p>关注点分离原则一个很重要的受益处在于可测试性。让我们看看每个模块代码的测试。</p>
<ul>
<li><strong>UI&amp;交互：</strong>这是唯一需要[Android UI Instrumentation test]的时刻。测试UI的最佳方式是创建一个[Espresso]特使。你可以创建Fragment并提供一个虚拟的ViewModel。因为Fragment仅仅和ViewModel对话，模拟ViewModel对于测试来说就已经足够了。</li>
<li><strong>ViewModel：</strong>ViewModel可以使用[JUnit测试]。你仅仅需要模拟UserRepository。</li>
<li><strong>UserRepository：</strong>你也可以使用JUnit测试UserRepository 。你需要模拟Webservice和DAO。你可以测试网络请求调用，在数据库中保存结果，以及如果数据被缓存并更新后不需要进行请求。因为Webservice和UserDao都是接口，你可以模拟它们。</li>
<li><strong>UserDao：</strong>测试DAO类的推荐方法是使用测试工具。因为这些测试工具并不需要任何的UI并运行速度很快。对每个测试来说，你可以创建一个内存数据库来保证测试并不会造成双边效应（如改变磁盘上数据库的已有数据）。</li>
<li><strong>WebService：</strong>独立于外部世界的测试是很重要的，甚至你的Webservice测试应该避免调用后台的网络服务。有大量的类库可以帮助做到这一点，例如：[MockWebServer]。</li>
<li><strong>测试构件：</strong>架构组件提供一个Maven构件来控制后台线程。在android.arch.core:core-testing中，有两个JUnit规则：<ul>
<li>任务立即执行规则：这个规则可用于强制架构组件在调用线程里立即执行任何后台操作</li>
<li>这个规则可用于工具测试，以等待架构组件的后台操作或者连接至Espresso作为闲置资源。</li>
</ul>
</li>
</ul>
<h2 id="最终架构"><a href="#最终架构" class="headerlink" title="最终架构"></a>最终架构</h2><p>下图显示了我们所推荐架构的所有模块，以及相互间的交互情况：<br><figure class="image-bubble">
                <div class="img-lightbox">
                    <div class="overlay"></div>
                    <img src="/2017/09/10/architecture-guide/final-architecture.png" alt="t1" title="">
                </div>
                <div class="image-caption">t1</div>
            </figure></p>
<h1 id="指导原则"><a href="#指导原则" class="headerlink" title="指导原则"></a>指导原则</h1><hr>
<p>以下的建议并不是强制性的，而是根据我们的经验得知，遵循这些建议会使你的代码更健壮，易于测试和易于维护。</p>
<ul>
<li>你在清单文件中所定义的入口点——Activity，Service，Broadcast Receiver等并不应该是数据源。相反，他们应该仅仅是和入库点相关的数据源子集。因为每个app的组件的存活时间都是短暂的、取决于用户的交互行为以及运行时整体上的健康度。</li>
<li>残忍坚决地创建良好的模块分界。例如，不要将从网络读取数据的代码扩展到多个类/包中。相似地，也不要将不相关职责的代码添加进来，如数据缓存等。（高内聚，低耦合）</li>
<li>模块间交互暴露的接口应该尽可能的少。不要尝试创建“仅仅用一次”的捷径，导致暴露一个模块的内部实现细节。你可能在短期会获益，但是在代码的演进过程中会耗费数倍的技术负担。</li>
<li>当你定义了模块间的交互时，考虑每个模块的单独可测试性。例如，有一个定义良好的用于从网络获取数据的API会更易于测试本地数据库持久化的模块。相反，如果你搞乱了两个模块间的逻辑，或将你网络请求的代码铺满了所有的地方，那么这将很难进行测试。</li>
<li>你app的核心是如何在其他app中变得突出。不要花费时间重复造轮子或一遍一遍地写相同的模板代码。相反，将你的心思花在如何使你的app独一无二，让Android架构组件以及其他推荐类库处理重复的部分。</li>
<li>持久化尽可能多和尽可能新鲜的数据，这样在离线模式下你的app也是可用的。你可能很享受高速的网络连接，可你的用户并不一定这样认为。</li>
<li>你的仓库应当指定单一数据源。当你的app需要访问数据时，应该永远来自于这个单一的数据源。</li>
</ul>
<h1 id="附加：显示网络状态"><a href="#附加：显示网络状态" class="headerlink" title="附加：显示网络状态"></a>附加：显示网络状态</h1><hr>
<p>在“推荐app架构”一节中，我们故意忽略了网络错误和加载状态，以使样例代码更简单。在本节中，我们致力于使用Resource类显示网络状态以及数据本身。</p>
<p>下面是样例的实现：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//一个描述数据以及其状态的泛型类</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Resource</span>&lt;<span class="title">T</span>&gt; </span>&#123;</span><br><span class="line">    <span class="meta">@NonNull</span> <span class="keyword">public</span> <span class="keyword">final</span> Status status;</span><br><span class="line">    <span class="meta">@Nullable</span> <span class="keyword">public</span> <span class="keyword">final</span> T data;</span><br><span class="line">    <span class="meta">@Nullable</span> <span class="keyword">public</span> <span class="keyword">final</span> String message;</span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="title">Resource</span><span class="params">(@NonNull Status status, @Nullable T data, @Nullable String message)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.status = status;</span><br><span class="line">        <span class="keyword">this</span>.data = data;</span><br><span class="line">        <span class="keyword">this</span>.message = message;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> &lt;T&gt; <span class="function">Resource&lt;T&gt; <span class="title">success</span><span class="params">(@NonNull T data)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> Resource&lt;&gt;(SUCCESS, data, <span class="keyword">null</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> &lt;T&gt; <span class="function">Resource&lt;T&gt; <span class="title">error</span><span class="params">(String msg, @Nullable T data)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> Resource&lt;&gt;(ERROR, data, msg);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> &lt;T&gt; <span class="function">Resource&lt;T&gt; <span class="title">loading</span><span class="params">(@Nullable T data)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> Resource&lt;&gt;(LOADING, data, <span class="keyword">null</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p>
<p>因为从网络加载数据并进行显示是一个常见的用例，我们创建了一个帮助类<code>NetworkBoundResource</code>可以在多个地方复用。下图是<code>NetworkBoundResource</code>的决策树：<br><figure class="image-bubble">
                <div class="img-lightbox">
                    <div class="overlay"></div>
                    <img src="/2017/09/10/architecture-guide/network-bound-resource.png" alt="t2" title="">
                </div>
                <div class="image-caption">t2</div>
            </figure></p>
<p>起点从观察数据源（数据库）开始。当入口被数据库第一次加载时，NetworkBoundResource检查结果是否足够良好以至于可以分发，并且/或应该从网络进行获取。注意，这二者可以同时发生，因为你可能想要显示缓存，同时从网络更新数据。</p>
<p>如果网络调用完全成功，保存结果至数据库并重新初始化数据流。如果网络请求失败，我们直接分发一个错误。</p>
<blockquote>
<p>将新的数据存储到磁盘以后，我们从数据库重新初始化数据流，但是通常我们并不需要这样做，因为数据库会分发这次变化。另一方面，依赖数据库去分发变化会是一把双刃剑，如果数据并没有变化，我们实际上可以避免这次分发。我们也不分发网络请求得到的数据，因为这违反了单一数据源的原则。</p>
</blockquote>
<p>以下是NetworkBoundResource所提供的API：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ResultType: 数据源类型</span></span><br><span class="line"><span class="comment">// RequestType: API返回的类型</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">NetworkBoundResource</span>&lt;<span class="title">ResultType</span>, <span class="title">RequestType</span>&gt; </span>&#123;</span><br><span class="line">    <span class="comment">// 被调用保存API返回的结果至数据库</span></span><br><span class="line">    <span class="meta">@WorkerThread</span></span><br><span class="line">    <span class="function"><span class="keyword">protected</span> <span class="keyword">abstract</span> <span class="keyword">void</span> <span class="title">saveCallResult</span><span class="params">(@NonNull RequestType item)</span></span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 被调用去判断是否应该从网络获取数据</span></span><br><span class="line">    <span class="meta">@MainThread</span></span><br><span class="line">    <span class="function"><span class="keyword">protected</span> <span class="keyword">abstract</span> <span class="keyword">boolean</span> <span class="title">shouldFetch</span><span class="params">(@Nullable ResultType data)</span></span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 被调用从数据库获取缓存数据</span></span><br><span class="line">    <span class="meta">@NonNull</span> <span class="meta">@MainThread</span></span><br><span class="line">    <span class="function"><span class="keyword">protected</span> <span class="keyword">abstract</span> LiveData&lt;ResultType&gt; <span class="title">loadFromDb</span><span class="params">()</span></span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 被调用创建API请求</span></span><br><span class="line">    <span class="meta">@NonNull</span> <span class="meta">@MainThread</span></span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">abstract</span> LiveData&lt;ApiResponse&lt;RequestType&gt;&gt; createCall();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 当获取数据失败时候调用</span></span><br><span class="line">    <span class="meta">@MainThread</span></span><br><span class="line">    <span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">onFetchFailed</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 返回代表数据源的LiveData</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">final</span> LiveData&lt;Resource&lt;ResultType&gt;&gt; getAsLiveData() &#123;</span><br><span class="line">        <span class="keyword">return</span> result;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p>
<p>注意，上面的类定义了两种类型的参数（ResultType和RequestType），因为从API返回的数据类型可能和本地的数据类型并不匹配。</p>
<p>同样也请注意，上面的代码使用了ApiResponse用于网络请求。ApiResponse 是Retrofit2.Call类的简单包装，用于将返回结果转化为LiveData。</p>
<p>下面的NetworkBoundResource的其余实现：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">NetworkBoundResource</span>&lt;<span class="title">ResultType</span>, <span class="title">RequestType</span>&gt; </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> MediatorLiveData&lt;Resource&lt;ResultType&gt;&gt; result = <span class="keyword">new</span> MediatorLiveData&lt;&gt;();</span><br><span class="line"></span><br><span class="line">    <span class="meta">@MainThread</span></span><br><span class="line">    NetworkBoundResource() &#123;</span><br><span class="line">        result.setValue(Resource.loading(<span class="keyword">null</span>));</span><br><span class="line">        LiveData&lt;ResultType&gt; dbSource = loadFromDb();</span><br><span class="line">        result.addSource(dbSource, data -&gt; &#123;</span><br><span class="line">            result.removeSource(dbSource);</span><br><span class="line">            <span class="keyword">if</span> (shouldFetch(data)) &#123;</span><br><span class="line">                fetchFromNetwork(dbSource);</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                result.addSource(dbSource,</span><br><span class="line">                        newData -&gt; result.setValue(Resource.success(newData)));</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">fetchFromNetwork</span><span class="params">(<span class="keyword">final</span> LiveData&lt;ResultType&gt; dbSource)</span> </span>&#123;</span><br><span class="line">        LiveData&lt;ApiResponse&lt;RequestType&gt;&gt; apiResponse = createCall();</span><br><span class="line">        <span class="comment">// 重新连接dbSource作为新的源,</span></span><br><span class="line">        <span class="comment">//这样会快速分发最新的数据</span></span><br><span class="line">        result.addSource(dbSource,</span><br><span class="line">                newData -&gt; result.setValue(Resource.loading(newData)));</span><br><span class="line">        result.addSource(apiResponse, response -&gt; &#123;</span><br><span class="line">            result.removeSource(apiResponse);</span><br><span class="line">            result.removeSource(dbSource);</span><br><span class="line">            <span class="keyword">if</span> (response.isSuccessful()) &#123;</span><br><span class="line">                saveResultAndReInit(response);</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                onFetchFailed();</span><br><span class="line">                result.addSource(dbSource,</span><br><span class="line">                        newData -&gt; result.setValue(</span><br><span class="line">                                Resource.error(response.errorMessage, newData)));</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@MainThread</span></span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">saveResultAndReInit</span><span class="params">(ApiResponse&lt;RequestType&gt; response)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">new</span> AsyncTask&lt;Void, Void, Void&gt;() &#123;</span><br><span class="line"></span><br><span class="line">            <span class="meta">@Override</span></span><br><span class="line">            <span class="function"><span class="keyword">protected</span> Void <span class="title">doInBackground</span><span class="params">(Void... voids)</span> </span>&#123;</span><br><span class="line">                saveCallResult(response.body);</span><br><span class="line">                <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="meta">@Override</span></span><br><span class="line">            <span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">onPostExecute</span><span class="params">(Void aVoid)</span> </span>&#123;</span><br><span class="line">                <span class="comment">// 我们专门请求一个新的LiveData</span></span><br><span class="line">                <span class="comment">// 另一方面获取最新的缓存数据，可能并不是网络请求得到的最新数据</span></span><br><span class="line">                result.addSource(loadFromDb(),</span><br><span class="line">                        newData -&gt; result.setValue(Resource.success(newData)));</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;.execute();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p>
<p>现在，我们使用NetworkBoundResource来重写UserRepository：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">UserRepository</span> </span>&#123;</span><br><span class="line">    Webservice webservice;</span><br><span class="line">    UserDao userDao;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> LiveData&lt;Resource&lt;User&gt;&gt; loadUser(<span class="keyword">final</span> String userId) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> NetworkBoundResource&lt;User,User&gt;() &#123;</span><br><span class="line">            <span class="meta">@Override</span></span><br><span class="line">            <span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">saveCallResult</span><span class="params">(@NonNull User item)</span> </span>&#123;</span><br><span class="line">                userDao.insert(item);</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="meta">@Override</span></span><br><span class="line">            <span class="function"><span class="keyword">protected</span> <span class="keyword">boolean</span> <span class="title">shouldFetch</span><span class="params">(@Nullable User data)</span> </span>&#123;</span><br><span class="line">                <span class="keyword">return</span> rateLimiter.canFetch(userId) &amp;&amp; (data == <span class="keyword">null</span> || !isFresh(data));</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="meta">@NonNull</span> <span class="meta">@Override</span></span><br><span class="line">            <span class="function"><span class="keyword">protected</span> LiveData&lt;User&gt; <span class="title">loadFromDb</span><span class="params">()</span> </span>&#123;</span><br><span class="line">                <span class="keyword">return</span> userDao.load(userId);</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="meta">@NonNull</span> <span class="meta">@Override</span></span><br><span class="line">            <span class="keyword">protected</span> LiveData&lt;ApiResponse&lt;User&gt;&gt; createCall() &#123;</span><br><span class="line">                <span class="keyword">return</span> webservice.getUser(userId);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;.getAsLiveData();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p>

        </div>

        <blockquote class="post-copyright">
    
    <div class="content">
        
<span class="post-time">
    最后更新时间：<time datetime="2018-04-11T08:49:40.569Z" itemprop="dateUpdated">2018-04-11 16:49:40</time>
</span><br>


        
        如有错误，请留言指正。
        
    </div>
    
    <footer>
        <a href="http://yoursite.com">
            <img src="/img/avatar.jpg" alt="马建">
            马建
        </a>
    </footer>
</blockquote>

        
<div class="page-reward">
    <a id="rewardBtn" href="javascript:;" class="page-reward-btn waves-effect waves-circle waves-light">赏</a>
</div>



        <div class="post-footer">
            
	<ul class="article-tag-list"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Android/">Android</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Architecture-Components/">Architecture Components</a></li></ul>


            
<div class="page-share-wrap">
    

<div class="page-share" id="pageShare">
    <ul class="reset share-icons">
      <li>
        <a class="weibo share-sns" target="_blank" href="http://service.weibo.com/share/share.php?url=http://yoursite.com/2017/09/10/architecture-guide/&title=《Android架构组件(一)-指导》 — 随手记&pic=http://yoursite.com/img/avatar.jpg" data-title="微博">
          <i class="icon icon-weibo"></i>
        </a>
      </li>
      <li>
        <a class="weixin share-sns wxFab" href="javascript:;" data-title="微信">
          <i class="icon icon-weixin"></i>
        </a>
      </li>
      <li>
        <a class="qq share-sns" target="_blank" href="http://connect.qq.com/widget/shareqq/index.html?url=http://yoursite.com/2017/09/10/architecture-guide/&title=《Android架构组件(一)-指导》 — 随手记&source=desc" data-title=" QQ">
          <i class="icon icon-qq"></i>
        </a>
      </li>
      <li>
        <a class="facebook share-sns" target="_blank" href="https://www.facebook.com/sharer/sharer.php?u=http://yoursite.com/2017/09/10/architecture-guide/" data-title=" Facebook">
          <i class="icon icon-facebook"></i>
        </a>
      </li>
      <li>
        <a class="twitter share-sns" target="_blank" href="https://twitter.com/intent/tweet?text=《Android架构组件(一)-指导》 — 随手记&url=http://yoursite.com/2017/09/10/architecture-guide/&via=http://yoursite.com" data-title=" Twitter">
          <i class="icon icon-twitter"></i>
        </a>
      </li>
      <li>
        <a class="google share-sns" target="_blank" href="https://plus.google.com/share?url=http://yoursite.com/2017/09/10/architecture-guide/" data-title=" Google+">
          <i class="icon icon-google-plus"></i>
        </a>
      </li>
    </ul>
 </div>



    <a href="javascript:;" id="shareFab" class="page-share-fab waves-effect waves-circle">
        <i class="icon icon-share-alt icon-lg"></i>
    </a>
</div>



        </div>
    </div>

    
<nav class="post-nav flex-row flex-justify-between">
  
    <div class="waves-block waves-effect prev">
      <a href="/2017/09/10/architecture-adding-components/" id="post-prev" class="post-nav-link">
        <div class="tips"><i class="icon icon-angle-left icon-lg icon-pr"></i> Prev</div>
        <h4 class="title">Android架构组件(二)-添加组件</h4>
      </a>
    </div>
  

  
    <div class="waves-block waves-effect next">
      <a href="/2017/08/29/android-8-0/" id="post-next" class="post-nav-link">
        <div class="tips">Next <i class="icon icon-angle-right icon-lg icon-pl"></i></div>
        <h4 class="title">Android 8.0 功能和 API</h4>
      </a>
    </div>
  
</nav>



    














</article>

<div id="reward" class="page-modal reward-lay">
    <a class="close" href="javascript:;"><i class="icon icon-close"></i></a>
    <h3 class="reward-title">
        <i class="icon icon-quote-left"></i>
        谢谢大爷~
        <i class="icon icon-quote-right"></i>
    </h3>
    <div class="reward-content">
        
        <div class="reward-code">
            <img id="rewardCode" src="/img/wechat.jpg" alt="打赏二维码">
        </div>
        
        <label class="reward-toggle">
            <input id="rewardToggle" type="checkbox" class="reward-toggle-check"
                data-wechat="/img/wechat.jpg" data-alipay="/img/alipay.jpg">
            <div class="reward-toggle-ctrol">
                <span class="reward-toggle-item wechat">微信</span>
                <span class="reward-toggle-label"></span>
                <span class="reward-toggle-item alipay">支付宝</span>
            </div>
        </label>
        
    </div>
</div>



</div>

        <footer class="footer">
    <div class="top">
        
<p>
    <span id="busuanzi_container_site_uv" style='display:none'>
        站点总访客数：<span id="busuanzi_value_site_uv"></span>
    </span>
    <span id="busuanzi_container_site_pv" style='display:none'>
        站点总访问量：<span id="busuanzi_value_site_pv"></span>
    </span>
</p>


        <p>
            
                <span><a href="/atom.xml" target="_blank" class="rss" title="rss"><i class="icon icon-lg icon-rss"></i></a></span>
            
            <span>博客内容遵循 <a rel="license" href="https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh">知识共享 署名 - 非商业性 - 相同方式共享 4.0 国际协议</a></span>
        </p>
    </div>
    <div class="bottom">
        <p><span>马建 &copy; 2015 - 2019</span>
            <span>
                
            </span>
        </p>
    </div>
</footer>

    </main>
    <div class="mask" id="mask"></div>
<a href="javascript:;" id="gotop" class="waves-effect waves-circle waves-light"><span class="icon icon-lg icon-chevron-up"></span></a>



<div class="global-share" id="globalShare">
    <ul class="reset share-icons">
      <li>
        <a class="weibo share-sns" target="_blank" href="http://service.weibo.com/share/share.php?url=http://yoursite.com/2017/09/10/architecture-guide/&title=《Android架构组件(一)-指导》 — 随手记&pic=http://yoursite.com/img/avatar.jpg" data-title="微博">
          <i class="icon icon-weibo"></i>
        </a>
      </li>
      <li>
        <a class="weixin share-sns wxFab" href="javascript:;" data-title="微信">
          <i class="icon icon-weixin"></i>
        </a>
      </li>
      <li>
        <a class="qq share-sns" target="_blank" href="http://connect.qq.com/widget/shareqq/index.html?url=http://yoursite.com/2017/09/10/architecture-guide/&title=《Android架构组件(一)-指导》 — 随手记&source=desc" data-title=" QQ">
          <i class="icon icon-qq"></i>
        </a>
      </li>
      <li>
        <a class="facebook share-sns" target="_blank" href="https://www.facebook.com/sharer/sharer.php?u=http://yoursite.com/2017/09/10/architecture-guide/" data-title=" Facebook">
          <i class="icon icon-facebook"></i>
        </a>
      </li>
      <li>
        <a class="twitter share-sns" target="_blank" href="https://twitter.com/intent/tweet?text=《Android架构组件(一)-指导》 — 随手记&url=http://yoursite.com/2017/09/10/architecture-guide/&via=http://yoursite.com" data-title=" Twitter">
          <i class="icon icon-twitter"></i>
        </a>
      </li>
      <li>
        <a class="google share-sns" target="_blank" href="https://plus.google.com/share?url=http://yoursite.com/2017/09/10/architecture-guide/" data-title=" Google+">
          <i class="icon icon-google-plus"></i>
        </a>
      </li>
    </ul>
 </div>


<div class="page-modal wx-share" id="wxShare">
    <a class="close" href="javascript:;"><i class="icon icon-close"></i></a>
    <p>扫一扫，分享到微信</p>
    <img src="" alt="微信分享二维码">
</div>




    <script src="//cdn.bootcss.com/node-waves/0.7.4/waves.min.js"></script>
<script>
var BLOG = { ROOT: '/', SHARE: true, REWARD: true };


</script>

<script src="/js/main.min.js?v=1.7.2"></script>


<div class="search-panel" id="search-panel">
    <ul class="search-result" id="search-result"></ul>
</div>
<template id="search-tpl">
<li class="item">
    <a href="{path}" class="waves-block waves-effect">
        <div class="title ellipsis" title="{title}">{title}</div>
        <div class="flex-row flex-middle">
            <div class="tags ellipsis">
                {tags}
            </div>
            <time class="flex-col time">{date}</time>
        </div>
    </a>
</li>
</template>

<script src="/js/search.min.js?v=1.7.2" async></script>






<script async src="//dn-lbstatics.qbox.me/busuanzi/2.3/busuanzi.pure.mini.js"></script>



<script>
(function() {
    var OriginTitile = document.title, titleTime;
    document.addEventListener('visibilitychange', function() {
        if (document.hidden) {
            document.title = '随手记';
            clearTimeout(titleTime);
        } else {
            document.title = '随手记';
            titleTime = setTimeout(function() {
                document.title = OriginTitile;
            },2000);
        }
    });
})();
</script>



</body>
</html>
